# frozen_string_literal: true

require 'ipaddr'
require 'json'

require 'logger'
$logger = Logger.new(STDOUT)

module AWDB
  class Data
    def initialize(database)
      @file_byte = File.new(database, 'rb')
      @size = @file_byte.size
      @metadata_len, = parse_int(0, 2)
      @metadata_map = JSON.parse(@file_byte.pread(@metadata_len, 2))
      @node_count = @metadata_map['node_count']
      @ip_version = @metadata_map['ip_version']
      @byte_len = @metadata_map['byte_len']
      @columns = @metadata_map['columns']
      @decode_type = @metadata_map['decode_type']
      @start_len = 2 + @metadata_len
      @base_offset = @node_count * @byte_len * 2 + @start_len
    end

    DTM = {
      1 => :parse_array,
      2 => :parse_pointer,
      3 => :parse_string,
      4 => :parse_long_string,
      5 => :parse_uint_bytes,
      6 => :parse_int,
      7 => :parse_float,
      8 => :parse_double,
    }.freeze
    public_constant :DTM

    def get_metadata()
      return @metadata_map
    end

    def parse_array(offset)
      data_len, = parse_uint(offset)
      offset += 1
      array = Array.new
      while data_len > 0 do
        data_len -= 1
        value, offset = decode_content(offset)
        array.append(value)
      end
      return array, offset
    end

    def parse_pointer(offset)
      data_len, = parse_uint(offset)
      # print("pointer data_len ", data_len , "\n" )
      offset += 1
      new_offset = offset + data_len
      buf, = parse_int_bytes(offset, data_len)
      pointer = @base_offset + buf
      value, _ = self.decode_content(pointer)
      return value, new_offset
    end

    def parse_string(offset)
      data_len, = parse_uint(offset)
      offset += 1
      buf = @file_byte.pread(data_len, offset)
      return buf.force_encoding(Encoding::UTF_8), offset + data_len
    end

    def parse_long_string(offset)
      size, = parse_uint(offset)
      offset += 1
      data_len, = parse_uint(offset, size)
      offset += size
      buf = @file_byte.pread(data_len, offset)
      return buf.force_encoding(Encoding::UTF_8), offset + data_len
    end

    def parse_uint(offset, size = 1, type_code = 'n', type_size = 2)
      return 0, offset if size == 0
      buf = @file_byte.pread(size, offset)
      buf = buf.rjust(type_size, "\x00") if size != type_size
      if size > 1
        _buf = buf.rjust(8, "\x00")
        return _buf.unpack1('Q>'), offset + size
      end
      return buf.unpack1(type_code), offset + size
    end

    def parse_uint_bytes(offset)
      data_len, = parse_uint(offset)
      offset += 1
      uint_value, = parse_uint(offset, data_len)
      return uint_value, offset + data_len
    end

    def parse_int(offset, size = 4, type_code = 'l>', type_size = 4)
      return 0, offset if size == 0
      buf = @file_byte.pread(size, offset)
      buf = buf.rjust(type_size, "\x00") if size != type_size
      int_value = buf.unpack1(type_code)
      return int_value, offset + size
    end

    def parse_int_bytes(offset, data_len)
      return parse_uint(offset) if data_len == 1
      value = 0
      while data_len > 0 do
        data_len -= 1
        _value, offset = parse_uint(offset)
        value += _value * (256 ** data_len)
      end
      return value, offset
    end

    def parse_float(offset, size = 4, type_code = 'g', type_size = 4)
      return 0, offset if size == 0
      buf = @file_byte.pread(size, offset)
      buf = buf.rjust(type_size, "\x00") if size != type_size
      return buf.unpack1(type_code), offset + size
    end

    def parse_double(offset, size = 8, type_code = 'G', type_size = 8)
      return 0, offset if size == 0
      buf = @file_byte.pread(size, offset)
      buf = buf.rjust(type_size, "\x00") if size != type_size
      return buf.unpack1(type_code), offset + size
    end

    def decode_content(offset)
      # data_type, = parse_uint(offset)
      buf = @file_byte.pread(1, offset)
      data_type = buf.ord & 0xFF
      return send(DTM[data_type], offset + 1)
    end

    def iptoint(ip)
      if ip.include?(".") or ip.include?(":")
        ip = IPAddr.new(ip)
        if ip.ipv4?() and @ip_version == "6"
          $logger.error("The database is IPv6 library, but you are using ipv4 queries!")
          exit()
        elsif ip.ipv6?() and @ip_version == "4"
          $logger.error("The database is IPv4 library, but you are using ipv6 queries!")
          exit()
        end
        if @ip_version == "4"
          return ip.to_i, 32
        elsif ip.ipv6?()
          return ip.to_i, 128
        elsif @ip_version == "4_6"
          return ip.to_i + 281470681743360, 128
        else
          $logger.error("ValueError")
          exit()
        end
      else
        ip = ip.to_i
        if ip < 2 ** 32 and @ip_version == "6"
          $logger.error("The database is IPv6 library, but you are using ipv4 queries!")
          exit()
        elsif ip >= 2 ** 32 and @ip_version == "4"
          $logger.error("The database is IPv4 library, but you are using ipv6 queries!")
          exit()
        end
        if @ip_version == "4"
          return ip, 32
        elsif @ip_version == "6"
          return ip, 128
        elsif @ip_version == "4_6" and ip >= 2 ** 32
          return ip, 128
        elsif @ip_version == "4_6" and ip < 2 ** 32
          return ip + 281470681743360, 128
        else
          $logger.error("ValueError")
          exit()
        end
      end
    end

    def find_tree_index(ip)
      ip_int, ip_count = iptoint(ip)
      node_index = 0
      if @ip_version == '4_6'
        if (ip_int >> 32) == 0xFFFF
          node_index = 96
        end
      end
      i = ip_count - node_index
      while i > 0 do
        i = i - 1
        if node_index >= @node_count
          break
        end
        bit_value = (ip_int >> i) & 1
        offset = node_index * @byte_len * 2 + bit_value * @byte_len + @start_len
        if @byte_len == 4
          node_index, = parse_int(offset, @byte_len)
        else
          node_index, = parse_int_bytes(offset, @byte_len)
        end

      end
      if node_index == @node_count
        $logger.error("Invalid node_index in search tree")
        return 0
      end
      return node_index if node_index > @node_count
    end

    def create_hash(keys, values)
      result = Hash.new
      i = 0
      while i < keys.size do
        if keys[i].class == Array
          i += 1
          next
        end
        if values[i].class == Array
          mu_res = Array.new
          values[i].each do |loc|
            mu_res.append(create_hash(keys[i + 1], loc))
          end
          result[keys[i]] = mu_res
        else
          if values[i].nil?
            result[keys[i]] = ""
          else
            result[keys[i]] = values[i]
          end
        end
        i += 1
      end
      return result
    end

    def decode_content_direct(offset)
      data_len, = parse_int_bytes(offset, 4)
      offset += 4
      value_string = @file_byte.pread(data_len, offset)
      value_string = value_string.force_encoding(Encoding::UTF_8)
      values = value_string.split("\t")
      return values
    end

    def locate(ip_address)
      node_index = find_tree_index(ip_address)
      if node_index <= 0
        return JSON.generate({})
      end

      resolved = @base_offset + node_index - @node_count - 10
      if @decode_type == 2
        content = decode_content_direct(resolved)
      else
        content, = decode_content(resolved)
      end
      result = create_hash(@columns, content)
      return result
    end

    public def close
          @file_byte.close
      end
    end
  end
